/* weehttpd
* Copyright (c) 2008 Jeffery Myers
*
* This package is free software;  you can redistribute it and/or
* modify it under the terms of the license found in the file
* named COPYING that should have accompanied this file.
*
* THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
*/

#include "httpd.h"
#include "textUtils.h"
#include "connection.h"
#include <algorithm>
#include <sstream>
#include <time.h>
#include <list>

class WeeHTTPServer: public WeeHTTPD
{
public:
	WeeHTTPServer(const WeeHTTPDOptions &opts);
	virtual ~WeeHTTPServer();

public:
	virtual bool registerVDir(WeeHTTPVDir* handler);
	virtual bool removeVDir(WeeHTTPVDir* handler);

	virtual void update ( void );
protected:
	WeeHTTPDOptions	options;

	std::map<std::string,WeeHTTPVDir*> vdirs;

	bool processRequest(WeeHTTPRequest &request, unsigned int connectionID);

	HTTPConnectionMap liveConnections;

	std::string baseURL;

private:
	void send100Continue(unsigned int connectionID);
	void send403Error(unsigned int connectionID);
	void send404Error(unsigned int connectionID);
	void send501Error(unsigned int connectionID);
	void sendOptions(unsigned int connectionID, bool p);

	void generateIndex(unsigned int connectionID, const WeeHTTPRequest &request);
	bool generatePage(WeeHTTPVDir* vdir, int connectionID, WeeHTTPRequest &request);

	void newConnection ( unsigned int connectionID );
	void pending(unsigned int connectionID, void *d, size_t s);

	std::list<unsigned int> activeConnections;

	unsigned int lastRequestID;

};

void WeeHTTPServer::newConnection ( unsigned int connectionID )
{
	activeConnections.push_back(connectionID);

	HTTPConnection connection(options);
	connection.connectionID = connectionID;
	connection.request = eUnknown;
	connection.sessionID = generateSessionID();  // new ID in case they don't have one

	HTTPConnectionMap::iterator itr = liveConnections.find(connection.connectionID);

	if (itr != liveConnections.end())
		liveConnections.erase(itr);	// something weird is happening here

	liveConnections[connection.connectionID] = connection;
}

void WeeHTTPServer::pending(unsigned int connectionID, void *d, size_t s)
{
	HTTPConnectionMap::iterator itr = liveConnections.find(connectionID);

	if (itr == liveConnections.end())
		return;

	HTTPConnection &connection = itr->second;

	// grab the current data
	if(d && s)
	{
		char *t = (char*)malloc(s+1);
		memcpy(t,d,s);
		t[s] = 0;
		connection.currentData += t;
		free(t);
	}

	// see what our status is
	if (!connection.request)
	{
		std::stringstream stream(connection.currentData);

		std::string request, resource, httpVersion;
		stream >> request >> resource >> httpVersion;

		if (request.size() && resource.size() && httpVersion.size())
		{
			if (compare_nocase(request,"get") == 0)
				connection.request = eGet;
			else if (compare_nocase(request,"head") == 0)
				connection.request = eHead;
			else if (compare_nocase(request,"post") == 0)
				connection.request = ePost;
			else if (compare_nocase(request,"put") == 0)
				connection.request = ePut;
			else if (compare_nocase(request,"delete") == 0)
				connection.request = eDelete;
			else if (compare_nocase(request,"trace") == 0)
				connection.request = eTrace;
			else if (compare_nocase(request,"options") == 0)
				connection.request = eOptions;
			else if (compare_nocase(request,"connect") == 0)
				connection.request = eConnect;
			else
				connection.request = eOther;

			if (resource.size() > 1) 
			{
				size_t p = resource.find_first_of('/');
				if (p != std::string::npos)
				{
					if (p == 0)
						p = resource.find_first_of('/',p+1);

					if (p == std::string::npos)
					{
						// there is only one / so the stuff after the slash in the vdir and the resource is NULL
						connection.vdir.resize(resource.size()-1);
						std::copy(resource.begin()+1,resource.end(),connection.vdir.begin());
					}
					else 
					{
						connection.vdir.resize(p-1);
						std::copy(resource.begin()+1,resource.begin()+p,connection.vdir.begin());

						connection.resource.resize(resource.size()-p-1);
						std::copy(resource.begin()+p+1,resource.end(),connection.resource.begin());
					}
				}
			}
		}
	}

	if (connection.request)
	{
		// we know the type, so we can get the rest of the data or bail out
		if (!connection.requestComplete &&
			(connection.request == ePost || connection.request == ePut)) // if the request is a post, tell the client to send us the rest of the body
			send100Continue(connectionID);

		size_t headerEnd = find_first_substr(connection.currentData,"\r\n\r\n");

		if (!connection.headerComplete && headerEnd != std::string::npos) 
		{
			bool done = false;  // ok we have the header and we don't haven't processed it yet

			// read past the command
			size_t p = find_first_substr(connection.currentData,"\r\n");
			p+= 2;

			while (p < headerEnd) 
			{
				size_t p2 = find_first_substr(connection.currentData,"\r\n",p);

				std::string line ( connection.currentData.substr(p, p2-p) );
				p = p2+2;

				trimLeadingWhitespace(line);
				std::vector<std::string> headerLine = tokenize(line,":",2,false);
				if (headerLine.size() > 1) 
				{
					std::string &key = headerLine[0];
					trimLeadingWhitespace(headerLine[1]);
					if (compare_nocase(key,"Host") == 0)
						connection.host = line.c_str()+key.size()+2;
					else if (compare_nocase(key,"Content-Length") == 0)
						connection.contentSize = (size_t)atoi(headerLine[1].c_str());
					else
						connection.header[key] = headerLine[1];
				}
			}
			connection.headerComplete = true;
		}

		if (connection.headerComplete && !connection.requestComplete) {
			connection.bodyEnd = headerEnd+4;

			if (connection.request != ePost && connection.request != ePut)
			{
				connection.requestComplete = true; // there is nothing after the header we care about
			}
			else 
			{
				if (connection.contentSize)
				{
					headerEnd += 4;
					if (connection.currentData.size()-headerEnd >= connection.contentSize)
					{
						// read in that body!
						connection.body.append( connection.currentData.substr(headerEnd) );
						connection.requestComplete = true;

						connection.bodyEnd += connection.contentSize;
					}
				}
				else
					connection.requestComplete = true;
			}
		}
	}

	if (connection.requestComplete) 
	{
		// special, if it's a trace, just fire it back to them
		if (connection.request == eTrace) 
		{
			options.networking->send(connectionID, connection.currentData.c_str(),connection.bodyEnd);

			connection.currentData.erase(0, connection.bodyEnd);
			connection.flush();
		} 
		else
		{
			// parse it all UP and build up a complete request
			connection.currentData.erase(0, connection.bodyEnd);

			WeeHTTPRequest request;
			connection.fillRequest(request);
			// rip off what we need for the request, and then flush
			if(processRequest(request,connectionID)) // the request closed, then just kill it
				return;
			connection.flush();
		}

		// if there are lines to read
		if (connection.currentData.size() && find_first_substr(connection.currentData,"\r\n") != std::string::npos)
			pending(connectionID,NULL,0);
	}
}

void WeeHTTPServer::update(void)
{
	// see if anything is new
	unsigned int conID = 0;
	while (options.networking->accept(conID))
		newConnection(conID);

	// see if anyone has any data to get
	std::list<unsigned int>::iterator conItr = activeConnections.begin();
	while (conItr != activeConnections.end())
	{
		unsigned int id = *conItr;
		if (options.networking->isOpen(id))
		{
			size_t s = options.networking->receive(id);
			if (s)
			{
				void * p = malloc(s);
				options.networking->receive(id,p);
				pending(id,p,s);
				free(p);
			}
			conItr++;
		}
		else
		{
			options.networking->close(id);
			conItr = activeConnections.erase(conItr);

			HTTPConnectionMap::iterator itr = liveConnections.find(id);

			if (itr != liveConnections.end())
				liveConnections.erase(itr);
		}
	}

	// check for outbound traffic on any of the HTTP connections
	HTTPConnectionMap::iterator itr = liveConnections.begin();

	while (itr != liveConnections.end())
	{
		if(itr->second.update())
		{
			HTTPConnectionMap::iterator i = itr;
			itr++;
			options.networking->close(i->second.connectionID);
		}
		else
			itr++;
	}
}

void WeeHTTPServer::generateIndex(unsigned int connectionID, const WeeHTTPRequest &request)
{
	HTTPConnectionMap::iterator itr = liveConnections.find(connectionID);

	if (itr == liveConnections.end())
		return;

	HTTPConnection &connection = itr->second;

	WeeHTTPReply reply;

	reply.docType = WeeHTTPReply::eHTML;
	reply.returnCode = WeeHTTPReply::e200OK;
	reply.body = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\"><html><head><title>Index page for " + baseURL + "</title></head>";
	reply.body += "<body>";

	std::map<std::string,WeeHTTPVDir*>::iterator dirItr = vdirs.begin();

	while (dirItr != vdirs.end())
	{
		std::string vdirName = dirItr->second->getVDir();
		std::string vDirDescription = dirItr->second->getDescription();
		reply.body += "<a href=\"/" + vdirName + "/\">" + vdirName +"</a>&nbsp;" +vDirDescription +"<br>";
		dirItr++;
	}

	reply.body += "</body></html>";

	connection.processingTasks.push_back(HTTPConnection::HTTPTask(reply,options,request.request == eHead));
	connection.update();
}

bool WeeHTTPServer::generatePage(WeeHTTPVDir* vdir, int connectionID, WeeHTTPRequest &request)
{
	HTTPConnectionMap::iterator itr = liveConnections.find(connectionID);

	if (itr == liveConnections.end())
		return false;

	HTTPConnection &connection = itr->second;

	WeeHTTPReply reply;

	request.baseURL = baseURL;
	request.baseURL += vdir->getVDir();
	request.baseURL += "/";
	request.requestID = lastRequestID++;
	request.sessionID = connection.sessionID;

	if (vdir->handleRequest(request,reply)) {
		reply.cookies["SessionID"] = format("%d",request.sessionID);
		connection.processingTasks.push_back(HTTPConnection::HTTPTask(reply,options,request.request == eHead));
	} else {
		connection.pendingTasks.push_back(HTTPConnection::PendingHTTPTask(reply,request,vdir,options,request.request == eHead));
	}

	if(connection.update())
	{
		connection.flush();
		options.networking->close(connectionID);
		liveConnections.erase(itr);
		return true;
	}

	return false;
}

bool WeeHTTPServer::processRequest(WeeHTTPRequest &request, unsigned int connectionID)
{
	// check the request to see if it'll have any thing we care to process

	// find the vdir handler

	WeeHTTPVDir *vdir = NULL;

	std::map<std::string,WeeHTTPVDir*>::iterator itr = vdirs.find(request.vdir);

	// check for a specific vdir
	if (itr != vdirs.end())
		vdir = itr->second;
	else
	{
		itr = vdirs.find("*"); // otherwise check for an "all" vdir
		if (itr != vdirs.end())
			vdir = itr->second;
	}

	switch(request.request)
	{
	case ePut:
		if (!vdir || !vdir->supportPut())
		{
			send403Error(connectionID);
			break;
		}
	case eHead:
	case eGet:
	case ePost:
		if(!vdir) // if there is no vdir, then there isn't an all handler, so list what we have
			generateIndex(connectionID,request);
		else
			return generatePage(vdir,connectionID,request);
		break;

	case eOptions:
		sendOptions(connectionID, vdir ? vdir->supportPut() : false);
		break;

	case eDelete:
	case eConnect:
		send501Error(connectionID);
		break;
	}

	return false;
}

void WeeHTTPServer::send100Continue(unsigned int connectionID)
{
	std::string httpHeaders;
	httpHeaders += "HTTP/1.1 100 Continue\n\n";

	options.networking->send(connectionID,httpHeaders.c_str(),httpHeaders.size());
}

void WeeHTTPServer::send403Error(unsigned int connectionID)
{
	std::string httpHeaders;
	httpHeaders += "HTTP/1.1 403 Forbidden\n\n";

	options.networking->send(connectionID,httpHeaders.c_str(),httpHeaders.size());
}

void WeeHTTPServer::send404Error(unsigned int connectionID)
{
	std::string httpHeaders;
	httpHeaders += "HTTP/1.1 404 Not Found\n\n";

	options.networking->send(connectionID,httpHeaders.c_str(),httpHeaders.size());
}

void WeeHTTPServer::send501Error(unsigned int connectionID)
{
	std::string httpHeaders;
	httpHeaders += "HTTP/1.1 501 Not Implemented\n\n";

	options.networking->send(connectionID,httpHeaders.c_str(),httpHeaders.size());
}

void WeeHTTPServer::sendOptions(unsigned int connectionID, bool p)
{
	std::string httpHeaders;
	httpHeaders += "HTTP/1.1 200 Ok\n";
	httpHeaders += "Allow: GET, HEAD, POST, OPTIONS";
	if (p)
		httpHeaders += ", PUT";
	httpHeaders += "\n\n";

	options.networking->send(connectionID,httpHeaders.c_str(),httpHeaders.size());
}

WeeHTTPServer::WeeHTTPServer(const WeeHTTPDOptions &opts)
{
	options = opts;
	for(size_t i = 0; i < opts.vdirs.size(); i++)
		registerVDir(opts.vdirs[i]);

	options.vdirs.clear();

	lastRequestID = 0;

	baseURL = "http://";
	baseURL += options.rootHost +"/";

}

WeeHTTPServer::~WeeHTTPServer()
{

}

bool WeeHTTPServer::registerVDir(WeeHTTPVDir* handler)
{
	if (!handler)
		return false;

	std::string name = handler->getVDir();
	if (!name.size())
		return false;

	if (vdirs.find(name) != vdirs.end())
		return false;

	vdirs[name] = handler;
	return true;
}

bool WeeHTTPServer::removeVDir(WeeHTTPVDir* handler)
{
	if (!handler)
		return false;

	std::string name = handler->getVDir();
	if (!name.size())
		return false;

	if (vdirs.find(name) == vdirs.end())
		return false;

	vdirs.erase(vdirs.find(name));
	return true;

}

WeeHTTPD *startHPPT(const WeeHTTPDOptions& options)
{
	return new WeeHTTPServer(options);
}

bool stopHTTPD(WeeHTTPD* httpd)
{
	WeeHTTPServer *server = (WeeHTTPServer*)httpd;
	delete(server);
	return (httpd != NULL);
}

// Local Variables: ***
// mode: C++ ***
// tab-width: 8 ***
// c-basic-offset: 2 ***
// indent-tabs-mode: t ***
// End: ***
// ex: shiftwidth=2 tabstop=8